import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import logging
import datetime
import os
import time  # 确保在文件开头导入time模块
import colorama  # 需要先安装: pip install colorama


class WebUIAutomation:
    def __init__(self, excel_path):
        """初始化WebUI自动化测试类"""
        try:
            # 确保在使用colorama之前初始化
            colorama.init()

            self.driver = webdriver.Firefox()
            self.test_data = pd.read_excel(excel_path)
            self.base_url = None
            self.setup_logging()
            self.test_results = []
            self.screenshot_dir = self.create_screenshot_dir()

            # 打印Excel列名，用于调试
            self.logger.info(f"Excel文件包含的列: {list(self.test_data.columns)}")

        except FileNotFoundError:
            raise FileNotFoundError(f"找不到Excel文件: {excel_path}")
        except Exception as e:
            raise Exception(f"初始化失败: {str(e)}")

    def setup_logging(self):
        """设置日志记录"""
        # 定义颜色代码
        GREEN = colorama.Fore.GREEN
        RED = colorama.Fore.RED
        RESET = colorama.Fore.RESET

        log_dir = '../logs'
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)

        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        log_file = f'{log_dir}/test_{timestamp}.log'

        class ColoredFormatter(logging.Formatter):
            def format(self, record):
                if record.levelno == logging.ERROR:
                    record.msg = f"{RED}{record.msg}{RESET}"
                elif "成功" in str(record.msg) or "Pass" in str(record.msg):
                    record.msg = f"{GREEN}{record.msg}{RESET}"
                return super().format(record)

        # 创建格式化器
        formatter = ColoredFormatter('%(asctime)s - %(levelname)s - %(message)s')

        # 设置文件处理器
        file_handler = logging.FileHandler(log_file, encoding='utf-8')
        file_handler.setFormatter(formatter)

        # 设置控制台处理器
        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)

        # 配置日志记录器
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.INFO)
        logger.addHandler(file_handler)
        logger.addHandler(console_handler)

        self.logger = logger

    def create_screenshot_dir(self):
        """创建截图目录"""
        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        screenshot_dir = f'screenshots_{timestamp}'
        if not os.path.exists(screenshot_dir):
            os.makedirs(screenshot_dir)
        return screenshot_dir

    def take_screenshot(self, test_case):
        """截图功能"""
        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"{self.screenshot_dir}/{test_case['测试用例编号']}_{test_case['步骤编号']}_{timestamp}.png"
        self.driver.save_screenshot(filename)
        return filename

    def execute_test_step(self, test_case):
        try:
            self.logger.info(f"开始执行测试用例 {test_case['测试用例编号']} 步骤 {test_case['步骤编号']}")

            # 对于等待操作，不需要先获取元素
            if test_case['测试步骤关键字'] == '等待':
                result = self.execute_keyword(test_case, None)
            else:
                # 其他操作需要先获取元素
                element = self.wait_for_element_presence(
                    test_case['定位方式'],
                    test_case['定位地址']
                )
                result = self.execute_keyword(test_case, element)

            # 截图
            screenshot = self.take_screenshot(test_case)

            # 记录测试结果
            self.test_results.append({
                '测试用例编号': test_case['测试用例编号'],
                '步骤编号': test_case['步骤编号'],
                '测试步骤': test_case['测试步骤'],
                '执行结果': 'Pass',
                '错误信息': '',
                '截图': screenshot
            })

            self.logger.info(f"✓ 测试用例 {test_case['测试用例编号']} 步骤 {test_case['步骤编号']} 执行成功")

        except Exception as e:
            error_msg = str(e)
            self.logger.error(f"✗ 测试用例 {test_case['测试用例编号']} 步骤 {test_case['步骤编号']} 执行失败: {error_msg}")
            screenshot = self.take_screenshot(test_case)

            self.test_results.append({
                '测试用例编号': test_case['测试用例编号'],
                '步骤编号': test_case['步骤编号'],
                '测试步骤': test_case['测试步骤'],
                '执行结果': 'Fail',
                '错误信息': error_msg,
                '截图': screenshot
            })

    def execute_keyword(self, test_case, element):
        """执行关键字操作"""
        keyword = test_case['测试步骤关键字']
        value = test_case['参数值']
        expected_result = test_case.get('预期结果', '')

        keyword_actions = {
            '点击': lambda: element.click(),
            '输入': lambda: element.send_keys(value),
            '清除': lambda: element.clear(),
            '双击': lambda: ActionChains(self.driver).double_click(element).perform(),
            '右击': lambda: ActionChains(self.driver).context_click(element).perform(),
            '悬停': lambda: ActionChains(self.driver).move_to_element(element).perform(),
            '获取文本': lambda: element.text,
            '获取属性': lambda: element.get_attribute(value),
            '按键': lambda: element.send_keys(getattr(Keys, value)),
            '提交': lambda: element.submit(),
            '判断是否显示': lambda: element.is_displayed(),
            '判断是否启用': lambda: element.is_enabled(),
            '判断是否选中': lambda: element.is_selected(),
            '切换框架': lambda: self.driver.switch_to.frame(element),
            '返回默认框架': lambda: self.driver.switch_to.default_content(),
            '切换窗口': lambda: self.switch_window(value),
            '执行JS': lambda: self.driver.execute_script(value, element),
            '等待': lambda: self.wait_sleep(value),  # 强制等待
            '等待元素可见': lambda: self.wait_for_element(test_case['定位方式'], test_case['定位地址'], float(value or 10)),
            '等待元素可点击': lambda: self.wait_for_element_clickable(test_case['定位方式'], test_case['定位地址'],
                                                               float(value or 10)),
            '等待元素存在': lambda: self.wait_for_element_presence(test_case['定位方式'], test_case['定位地址'], float(value or 10)),
            '等待文本出现': lambda: self.wait_for_text_present(test_case['定位方式'], test_case['定位地址'], expected_result,
                                                         float(value or 10)),
        }

        if keyword in keyword_actions:
            return keyword_actions[keyword]()
        else:
            raise ValueError(f"不支持的关键字操作: {keyword}")

    def switch_window(self, window_index):
        """切换窗口"""
        handles = self.driver.window_handles
        if window_index == '最新':
            self.driver.switch_to.window(handles[-1])
        elif window_index.isdigit() and int(window_index) < len(handles):
            self.driver.switch_to.window(handles[int(window_index)])

    def generate_report(self):
        """生成HTML测试报告"""
        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        report_file = f'test_report_{timestamp}.html'

        # 按测试用例编号分组统计
        test_cases = {}
        for result in self.test_results:
            case_id = result['测试用例编号']
            if case_id not in test_cases:
                test_cases[case_id] = {'steps': [], 'status': 'Pass'}
            test_cases[case_id]['steps'].append(result)
            # 如果任何步骤失败，整个用例就算失败
            if result['执行结果'] == 'Fail':
                test_cases[case_id]['status'] = 'Fail'

        # 统计用例级别的结果
        total_cases = len(test_cases)
        passed_cases = sum(1 for case in test_cases.values() if case['status'] == 'Pass')
        failed_cases = total_cases - passed_cases
        pass_rate = (passed_cases / total_cases * 100) if total_cases > 0 else 0

        # 调试信息
        self.logger.info(f"用例统计 - 总数: {total_cases}, 通过: {passed_cases}, 失败: {failed_cases}, 通过率: {pass_rate:.2f}%")

        html_content = f'''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Web UI自动化测试报告</title>
            <meta charset="utf-8">
            <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
            <style>
                body {{
                    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                    margin: 0;
                    padding: 20px;
                    background-color: #f5f5f5;
                }}
                .container {{
                    max-width: 1200px;
                    margin: 0 auto;
                    background-color: white;
                    padding: 20px;
                    border-radius: 10px;
                    box-shadow: 0 0 10px rgba(0,0,0,0.1);
                }}
                .header {{
                    text-align: center;
                    padding: 20px;
                    background: linear-gradient(135deg, #6e8efb, #a777e3);
                    color: white;
                    border-radius: 8px;
                    margin-bottom: 30px;
                }}
                .statistics-section {{
                    display: flex;
                    justify-content: space-around;
                    margin: 20px 0;
                    flex-wrap: wrap;
                }}
                .summary {{
                    text-align: center;
                    padding: 15px;
                    background-color: white;
                    border-radius: 8px;
                    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
                    min-width: 200px;
                    margin: 10px;
                }}
                .chart-container {{
                    width: 400px;
                    margin: 20px auto;
                }}
                .test-case {{
                    border: 1px solid #ddd;
                    margin-bottom: 15px;
                    padding: 15px;
                    border-radius: 8px;
                    transition: all 0.3s ease;
                }}
                .test-case:hover {{
                    transform: translateY(-2px);
                    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
                }}
                .pass {{
                    border-left: 5px solid #28a745;
                }}
                .fail {{
                    border-left: 5px solid #dc3545;
                }}
                .status-badge {{
                    display: inline-block;
                    padding: 5px 10px;
                    border-radius: 15px;
                    color: white;
                    font-weight: bold;
                }}
                .status-pass {{
                    background-color: #28a745;
                }}
                .status-fail {{
                    background-color: #dc3545;
                }}
                .screenshot {{
                    max-width: 100%;
                    height: auto;
                    border-radius: 5px;
                    margin-top: 10px;
                    border: 1px solid #ddd;
                }}
                .error-message {{
                    background-color: #fff3f3;
                    padding: 10px;
                    border-radius: 5px;
                    margin-top: 10px;
                    color: #dc3545;
                    font-family: monospace;
                }}
                .timestamp {{
                    color: #666;
                    font-size: 0.9em;
                }}
            </style>
        </head>
        <body>
            <div class="container">
                <div class="header">
                    <h1>Web UI自动化测试报告</h1>
                    <p>测试执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
                </div>

                <div class="statistics-section">
                    <div class="summary">
                        <div class="summary-item">
                            <h3>总用例数</h3>
                            <p>{total_cases}</p>
                        </div>
                        <div class="summary-item">
                            <h3>通过用例</h3>
                            <p style="color: #28a745">{passed_cases}</p>
                        </div>
                        <div class="summary-item">
                            <h3>失败用例</h3>
                            <p style="color: #dc3545">{failed_cases}</p>
                        </div>
                        <div class="summary-item">
                            <h3>通过率</h3>
                            <p>{pass_rate:.2f}%</p>
                        </div>
                    </div>

                    <div class="chart-container">
                        <canvas id="resultChart"></canvas>
                    </div>
                </div>

                <script>
                    // 确保数据不为空
                    const passedCases = {passed_cases};
                    const failedCases = {failed_cases};

                    // 创建饼图
                    var ctx = document.getElementById('resultChart').getContext('2d');
                    new Chart(ctx, {{
                        type: 'pie',
                        data: {{
                            labels: ['通过用例', '失败用例'],
                            datasets: [{{
                                data: [passedCases, failedCases],
                                backgroundColor: ['#28a745', '#dc3545'],
                                borderColor: ['#ffffff', '#ffffff'],
                                borderWidth: 2
                            }}]
                        }},
                        options: {{
                            responsive: true,
                            maintainAspectRatio: false,
                            plugins: {{
                                legend: {{
                                    position: 'bottom',
                                    labels: {{
                                        padding: 20,
                                        font: {{
                                            size: 14
                                        }}
                                    }}
                                }},
                                title: {{
                                    display: true,
                                    text: '测试用例执行结果统计',
                                    font: {{
                                        size: 16,
                                        weight: 'bold'
                                    }},
                                    padding: {{
                                        top: 10,
                                        bottom: 30
                                    }}
                                }},
                                tooltip: {{
                                    callbacks: {{
                                        label: function(context) {{
                                            let label = context.label || '';
                                            let value = context.raw || 0;
                                            let total = passedCases + failedCases;
                                            let percentage = ((value / total) * 100).toFixed(2);
                                            return `${{label}}: ${{value}} (${{percentage}}%)`;
                                        }}
                                    }}
                                }}
                            }}
                        }}
                    }});
                </script>
        '''

        # 添加测试用例详情
        for case_id, case_info in test_cases.items():
            case_status = case_info['status']
            status_class = 'pass' if case_status == 'Pass' else 'fail'

            html_content += f'''
                <div class="test-case {status_class}">
                    <h3>测试用例: {case_id}</h3>
                    <span class="status-badge status-{status_class.lower()}">{case_status}</span>
            '''

            # 添加每个步骤的详情
            for step in case_info['steps']:
                # 使用正确的字段名 '测试步骤'
                step_number = step.get('步骤编号', 'N/A')
                step_desc = step.get('测试步骤', 'N/A')  # 改回 '测试步骤'

                html_content += f'''
                    <div class="test-step">
                        <h4>步骤 {step_number}: {step_desc}</h4>
                        <span class="status-badge status-{step['执行结果'].lower()}">{step['执行结果']}</span>
                '''

                if step.get('错误信息'):
                    html_content += f'''
                        <div class="error-message">
                            <p>错误信息: {step['错误信息']}</p>
                        </div>
                    '''

                if step.get('截图'):
                    html_content += f'''
                        <img class="screenshot" src="{step['截图']}" alt="测试截图">
                    '''

                html_content += '</div>'

            html_content += '</div>'

        html_content += '''
            </div>
        </body>
        </html>
        '''

        with open(report_file, 'w', encoding='utf-8') as f:
            f.write(html_content)

        self.logger.info(f"测试报告已生成: {report_file}")

    def run_tests(self):
        """运行所有测试用例"""
        if not self.base_url:
            raise ValueError("请先使用set_base_url()设置基础URL")

        self.logger.info("开始执行测试套件")
        self.driver.get(self.base_url)

        # 打印Excel列名，用于调试
        self.logger.info(f"Excel文件包含的列: {list(self.test_data.columns)}")

        for index, row in self.test_data.iterrows():
            test_case = {}
            # 使用更灵活的列名匹配
            column_mappings = {
                '测试用例编号': ['测试用例编号', 'case_id', 'test_case_id'],
                '测试点': ['测试点', 'test_point'],
                '步骤编号': ['步骤编号', 'step_id', 'step_no'],
                '测试步骤': ['测试步骤', 'test_step'],
                '是否执行': ['是否执行', 'execute', 'run'],
                '测试步骤关键字': ['测试步骤关键字', 'keyword'],
                '定位方式': ['定位方式', 'locate_type', 'by'],
                '定位地址': ['定位地址', 'locate_value', 'value'],
                '参数值': ['参数值', 'parameter', 'param'],
                '预期结果': ['预期结果', 'expected_result', 'expected']
            }

            for key, possible_names in column_mappings.items():
                value = None
                for name in possible_names:
                    if name in row.index:
                        value = row[name]
                        break
                test_case[key] = str(value) if pd.notna(value) else ''

            # 检查必要的字段
            if not test_case['测试用例编号'] or not test_case['步骤编号']:
                self.logger.warning(f"跳过无效的测试用例行: {index + 1}")
                continue

            if test_case['是否执行'].upper() in ['Y', 'YES', '是', '1', 'TRUE', '']:
                self.execute_test_step(test_case)

        self.generate_report()
        self.logger.info("测试套件执行完成")

    def close(self):
        """关闭浏览器"""
        self.driver.quit()
        self.logger.info("测试环境清理完成")

    def set_base_url(self, url):
        """设置基础URL"""
        self.base_url = url
        self.logger.info(f"设置基础URL: {url}")

    def get_by_type(self, locate_type):
        """
        将定位方式字符串转换为 Selenium By 类型
        """
        locate_type = str(locate_type).lower()
        locate_types = {
            'id': By.ID,
            'name': By.NAME,
            'class': By.CLASS_NAME,
            'tag': By.TAG_NAME,
            'link': By.LINK_TEXT,
            'plink': By.PARTIAL_LINK_TEXT,
            'xpath': By.XPATH,
            'css': By.CSS_SELECTOR,
            # 支持中文定位方式
            'id定位': By.ID,
            'name定位': By.NAME,
            '类名定位': By.CLASS_NAME,
            '标签定位': By.TAG_NAME,
            '链接文本': By.LINK_TEXT,
            '部分链接文本': By.PARTIAL_LINK_TEXT,
            'xpath定位': By.XPATH,
            'css定位': By.CSS_SELECTOR
        }

        if locate_type not in locate_types:
            raise ValueError(f"不支持的定位方式: {locate_type}")

        return locate_types[locate_type]

    def wait_sleep(self, seconds):
        """
        强制等待指定的秒数
        """
        try:
            self.logger.info(f"等待 {seconds} 秒")
            time.sleep(float(seconds))
        except ValueError as e:
            self.logger.error(f"等待时间格式错误: {seconds}")
            raise e

    def verify_text(self, actual_text, expected_text, exact_match=True):
        """
        验证实际文本与预期文本是否匹配
        :param actual_text: 实际文本
        :param expected_text: 预期文本
        :param exact_match: 是否精确匹配，默认为True
        :return: bool
        """
        if not expected_text:  # 如果预期文本为空，则不进行验证
            return True

        actual_text = str(actual_text).strip()
        expected_text = str(expected_text).strip()

        self.logger.info(f"文本验证 - 实际文本: '{actual_text}', 预期文本: '{expected_text}'")

        if exact_match:
            result = actual_text == expected_text
            if not result:
                raise AssertionError(f"文本完全匹配失败: 预期'{expected_text}', 实际'{actual_text}'")
        else:
            result = expected_text in actual_text
            if not result:
                raise AssertionError(f"文本包含匹配失败: 预期文本'{expected_text}'未在实际文本'{actual_text}'中找到")

        return result

    def wait_for_element_presence(self, locate_type, locate_value, timeout=10):
        """
        等待元素在DOM中存在
        """
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.presence_of_element_located((
                    self.get_by_type(locate_type),
                    locate_value
                ))
            )
            return element
        except Exception as e:
            self.logger.error(f"等待元素存在超时: {locate_type}={locate_value}")
            raise e

    def wait_for_element(self, locate_type, locate_value, timeout=10):
        """
        等待元素可见
        """
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located((
                    self.get_by_type(locate_type),
                    locate_value
                ))
            )
            return element
        except Exception as e:
            self.logger.error(f"等待元素可见超时: {locate_type}={locate_value}")
            raise e

    def wait_for_element_clickable(self, locate_type, locate_value, timeout=10):
        """
        等待元素可点击
        """
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.element_to_be_clickable((
                    self.get_by_type(locate_type),
                    locate_value
                ))
            )
            return element
        except Exception as e:
            self.logger.error(f"等待元素可点击超时: {locate_type}={locate_value}")
            raise e

    def wait_for_text_present(self, locate_type, locate_value, text, timeout=10):
        """
        等待文本出现
        """
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.text_to_be_present_in_element((
                    self.get_by_type(locate_type),
                    locate_value
                ), text)
            )
            return element
        except Exception as e:
            self.logger.error(f"等待文本出现超时: {locate_type}={locate_value}, 期望文本: {text}")
            raise e


# 使用示例
if __name__ == "__main__":
    excel_path = "C:\\Users\\leixin\\Desktop\\web_test.xlsx"  # Excel文件路径
    automation = WebUIAutomation(excel_path)
    automation.set_base_url("https://www.baidu.com")  # 设置要测试的网站URL

    try:
        automation.run_tests()
    finally:
        automation.close()
